/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;
using DaveSexton.DocProject.Sandcastle.ContentManagement;
using DaveSexton.Controls;
using System.Xml;

namespace DaveSexton.DocProject.Sandcastle.TopicManagement
{
	internal sealed class TopicContentManager
	{
		#region Public Properties
		public bool IsDirty { get; private set; }
		#endregion

		#region Private / Protected
		private readonly Importer importer;
		private readonly CommentsManager comments;
		private readonly IDocProject project;
		private readonly XmlDocumentationDisplayManager displayManager;
		private readonly string mediaDirectoryProjectRelative;
		private readonly Dictionary<IXPathNavigable, XmlContentDocument> nodeCommentDocuments = new Dictionary<IXPathNavigable, XmlContentDocument>(64, XmlTopicComparer.Default);
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="TopicContentManager" /> class.
		/// </summary>
		public TopicContentManager(IDocProject project, string commentsPath, string projectXmlFileName, string mediaDirectoryProjectRelative,
			XmlDocumentationDisplayManager displayManager, Importer importer)
		{
			if (project == null)
				throw new ArgumentNullException("project");

			if (displayManager == null)
				throw new ArgumentNullException("displayManager");

			if (importer == null)
				throw new ArgumentNullException("importer");

			this.project = project;
			this.displayManager = displayManager;
			this.importer = importer;
			this.mediaDirectoryProjectRelative = mediaDirectoryProjectRelative;

			comments = new CommentsManager(commentsPath, projectXmlFileName);

			comments.FileCreated += (s, e) => project.GetItem(e.File).Show();

			List<string> markerOnlySections = new List<string>();

			foreach (XmlCommentSection section in SandcastleBuildEngineProvider.Configuration.DocumentationTags.Sections)
				if (section.IsMarkerOnly)
					markerOnlySections.Add(section.Name);

			comments.DeleteSection += (s, e) =>
				{
					if (markerOnlySections.Contains(e.Section))
						e.Cancel = true;
				};
		}
		#endregion

		#region Methods
		internal XmlContentDocument GetContentDocument(Topic topic)
		{
			if (topic == null)
				throw new ArgumentNullException("topic");

			if (nodeCommentDocuments.ContainsKey(topic.Element))
				return nodeCommentDocuments[topic.Element];

			if (topic.TopicType == TopicType.Conceptual || topic.TopicType == TopicType.Container || topic.TopicType == TopicType.Html)
			{
				ConceptualTopicSectionsDocument document = new ConceptualTopicSectionsDocument(new XmlConceptualTopic(topic));

				nodeCommentDocuments.Add(topic.Element, document);

				return document;
			}
			else
			{
				comments.BeginInit();

				try
				{
					CommentsMember member = comments[topic.Assembly][topic.Id];

					List<string> customItems = new List<string>(16);

					// NOTE: The items must be added to a list before being passed to the XmlCommentSectionDocument constructor since
					// the method that generates the list uses the C# iterator pattern, causing it to be lazily initialized, which means
					// that the customItems list will not be filled until after the document has been created.
					List<XmlCommentSectionContentItem> items = new List<XmlCommentSectionContentItem>(
						CreateContentSections(topic, member, customItems));

					XmlCommentSectionsDocument document = new XmlCommentSectionsDocument(new XmlCommentTopic(member), items, customItems);
					document.ItemChanged += new EventHandler<ContentItemEventArgs>(document_ItemChanged);

					nodeCommentDocuments.Add(topic.Element, document);

					return document;
				}
				finally
				{
					comments.EndInit();
				}
			}
		}

		private IEnumerable<XmlCommentSectionContentItem> CreateContentSections(Topic topic, CommentsMember member, List<string> customItems)
		{
			List<string> items = new List<string>(16);
			List<string> parameters = new List<string>(8);
			List<string> typeparameters = new List<string>(4);

			foreach (XmlCommentSectionContentItem item in CreateItemsForMember(member, parameters, typeparameters))
			{
				if (item.Multiple || !items.Contains(item.Name))
				{
					items.Add(item.Name);

					yield return item;
				}
			}

			foreach (XmlCommentSection section in SandcastleBuildEngineProvider.Configuration.DocumentationTags.Sections)
			{
				if (section.AppliesTo(topic.TopicType)
					&& (section.Subsections == null || section.Subsections.Count < 1)	// TODO: Remove the check for subsections (after support has been implemented)
					&& !string.Equals(section.Name, "param", StringComparison.Ordinal)
					&& !string.Equals(section.Name, "typeparam", StringComparison.Ordinal))
				{
					if (section.AllowMultiplePerTopic || section.IsMarkerOnly)
						customItems.Add(section.Name);
					else if (!items.Exists(delegate(string s) { return string.Equals(s, section.Name, StringComparison.OrdinalIgnoreCase); }))
					{
						XmlElement element = member.CreateSection(section.Name);
						yield return new XmlCommentSectionContentItem(element, section, null);
					}
				}
			}

			if (topic.TopicType == TopicType.Method || topic.TopicType == TopicType.Delegate)
			{
				foreach (string param in topic.MethodParameters)
				{
					if (!parameters.Contains(param))
					{
						XmlCommentSection section = SandcastleBuildEngineProvider.Configuration.DocumentationTags.FindSection("param");

						if (section != null)
						{
							XmlElement element = member.CreateSection(section.Name);
							yield return new XmlCommentSectionContentItem(element, section, param);
						}
					}
				}
			}

			if (topic.TopicType == TopicType.Method || topic.TopicType == TopicType.Delegate
				|| topic.TopicType == TopicType.Class || topic.TopicType == TopicType.Interface
				|| topic.TopicType == TopicType.Structure)
			{
				foreach (string param in topic.TypeParameters)
				{
					if (!typeparameters.Contains(param))
					{
						XmlCommentSection section = SandcastleBuildEngineProvider.Configuration.DocumentationTags.FindSection("typeparam");

						if (section != null)
						{
							XmlElement element = member.CreateSection(section.Name);
							yield return new XmlCommentSectionContentItem(element, section, param);
						}
					}
				}
			}
		}

		private IEnumerable<XmlCommentSectionContentItem> CreateItemsForMember(CommentsMember member, List<string> parameters, List<string> typeParameters)
		{
			foreach (XmlElement element in member)
			{
				string paramName = null, sectionName = element.Name;

				XmlCommentSection section = SandcastleBuildEngineProvider.Configuration.DocumentationTags.FindSection(sectionName);

				if (section == null)
				{
					List<ITopicSectionAttributeData> attributeData = null;

					IDictionary<string, string> attributes = member.GetAttributes(sectionName);

					if (attributes.Count > 0)
					{
						attributeData = new List<ITopicSectionAttributeData>(attributes.Count);

						foreach (KeyValuePair<string, string> attribute in attributes)
							if (!string.IsNullOrEmpty(attribute.Key))
								attributeData.Add(new XmlCommentSectionAttributeData(attribute.Key, TopicSectionAttributeType.String));
					}

					section = XmlCommentSection.CreateCustom(sectionName, attributeData);
				}
				else if (section.Subsections != null && section.Subsections.Count > 0)
					// TODO: implement support for elements that require subsections (e.g., the <overloads> element on methods)
					continue;
				else if (string.Equals("param", sectionName, StringComparison.OrdinalIgnoreCase))
				{
					paramName = element.GetAttribute("name");

					if (parameters.Contains(paramName))
						continue;

					parameters.Add(paramName);
				}
				else if (string.Equals("typeparam", sectionName, StringComparison.OrdinalIgnoreCase))
				{
					paramName = element.GetAttribute("name");

					if (typeParameters.Contains(paramName))
						continue;

					typeParameters.Add(paramName);
				}

				yield return new XmlCommentSectionContentItem(element, section, paramName);
			}
		}

		public void SaveChanges(IEnumerable<Topic> topics)
		{
			if (topics == null)
				throw new ArgumentNullException("topics");

			StringBuilder errorMessages = new StringBuilder();
			int count = 0, errorDisplayedCount = 0, totalErrors = 0, maxErrorsDisplayed = 5;

			IProjectItem mediaFolder = project.GetFolder(mediaDirectoryProjectRelative);
			mediaFolder.Checkout();

			foreach (Topic topic in topics)
			{
				count += ImportCommentsResources(topic, errorMessages, maxErrorsDisplayed,
					ref errorDisplayedCount, ref totalErrors);
			}

			if (count > 0)
			{
				if (errorDisplayedCount > 0)
				{
					if (errorDisplayedCount > maxErrorsDisplayed)
						errorMessages.AppendLine("... +" + (totalErrors - maxErrorsDisplayed) + " more");

					DocProjectEnvironment.ShowErrorDialogWithCaption("Topic Management Import Error",
						Resources.Errors.ImportImagesError, mediaDirectoryProjectRelative, Environment.NewLine, errorMessages);
				}

				mediaFolder.Refresh();
			}

			// NOTE: HasChanges only reports changes made by the user.  Comments don't have to be saved if the only change is rebased resource paths since
			// that's done automatically whenever images are present in documentation so that they are displayed correctly in the editor.
			if (comments.HasChanges)
				comments.Save(project);

			foreach (XmlContentDocument document in nodeCommentDocuments.Values)
			{
				project.GetItem(document.FilePath).Checkout();
				document.Save();
			}

			IsDirty = false;
		}

		public void CommitChanges()
		{
			bool dirty = false;

			foreach (XmlContentDocument document in nodeCommentDocuments.Values)
			{
				dirty = document.IsDirty || dirty;
				document.Commit();
			}

			if (dirty)
				IsDirty = true;
		}

		public void CancelChanges()
		{
			foreach (XmlContentDocument document in nodeCommentDocuments.Values)
				document.Cancel();

			IsDirty = false;
		}

		private int ImportCommentsResources(Topic topic, StringBuilder errorMessages, int maxErrorsDisplayed,
			ref int errorDisplayedCount, ref int totalErrors)
		{
			if (!nodeCommentDocuments.ContainsKey(topic.Element))
				return 0;

			XmlContentDocument document = GetContentDocument(topic);
			int count = 0;

			foreach (XmlCommentSectionContentItem section in document.Items)
			{
				if (section.IsDeleted || (section.EditModes & ContentItemEditModes.Design) == 0 && (section.EditModes & ContentItemEditModes.Source) == 0)
					continue;

				string html = section.Value;

				if (!string.IsNullOrEmpty(html))
				{
					count = importer.ImportImages(ref html);

					if (count > 0)
					{
						if (importer.HasErrors)
						{
							foreach (KeyValuePair<string, Exception> error in importer.ImportErrors)
							{
								Log.Exception(error.Value, "Topic Management - Resource import error for {0}", error.Key);

								if (++errorDisplayedCount <= maxErrorsDisplayed)
									errorMessages.AppendFormat("{0} - {1}{2}", error.Value.Message, error.Key, Environment.NewLine);
							}

							totalErrors += importer.ImportErrors.Count;
						}

						try
						{
							XmlCommentSectionContentItem xmlSection = section as XmlCommentSectionContentItem;

							if (xmlSection != null)
								xmlSection.RawValue = displayManager.ConvertHtmlToXml(html, false, null);
						}
						catch (ArgumentException ex)
						{
							errorMessages.AppendLine(ex.Message);
							errorDisplayedCount++;
							totalErrors++;
						}
					}
				}
			}

			return count;
		}
		#endregion

		#region Events
		private readonly object ItemChangedEventLock = new object();
		private EventHandler<ContentItemEventArgs> ItemChangedEvent;

		/// <summary>
		/// Evenr raised when an item's value changes.
		/// </summary>
		public event EventHandler<ContentItemEventArgs> ItemChanged
		{
			add
			{
				lock (ItemChangedEventLock)
				{
					ItemChangedEvent += value;
				}
			}
			remove
			{
				lock (ItemChangedEventLock)
				{
					ItemChangedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ItemChanged" /> event.
		/// </summary>
		/// <param name="e"><see cref="ContentItemEventArgs" /> object that provides the arguments for the event.</param>
		private void OnItemChanged(ContentItemEventArgs e)
		{
			EventHandler<ContentItemEventArgs> handler = null;

			lock (ItemChangedEventLock)
			{
				handler = ItemChangedEvent;
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region Event Handlers
		private void document_ItemChanged(object sender, ContentItemEventArgs e)
		{
			IsDirty = true;
			OnItemChanged(e);
		}
		#endregion
	}
}
